package neash;

import flash.display.Stage;
import flash.display.MovieClip;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.events.KeyboardEvent;
import flash.events.EventPhase;
import flash.display.DisplayObjectContainer;
import flash.display.DisplayObject;
import flash.display.InteractiveObject;
import flash.geom.Rectangle;

import neash.text.KeyCode;

#if flash

import flash.events.Event;

#elseif (neko||cpp)

import nme.Manager;
import nme.Surface;
import nme.Sprite;
import nme.Sound;
import nme.Music;
import nme.Rect;
import nme.Point;
import nme.Timer;

#elseif js

import canvas.Manager;
import canvas.geom.Point;
import js.Dom;

#end

/**
* @author	Hugh Sanderson
* @author	Lee Sylvester
* @author	Niel Drummond
* @author	Russell Weir
*/
class Lib
{
   var mKilled:Bool;
   static var mMe:Lib;
   public static var current(GetCurrent,null):MovieClip;
   public static var parameters(GetParameters,null):Dynamic;
   public static var debug = false;
   static var mShowCursor = true;
   static var mShowFPS = false;

   static var mDragObject:DisplayObject = null;
   static var mDragRect:Rectangle = null;
   static var mDragOffsetX:Float = 0;
   static var mDragOffsetY:Float = 0;
   static public var mOpenGL:Bool = false;
   var mRequestedWidth:Int;
   var mRequestedHeight:Int;
	var mResizePending:Bool;
   static var mFullscreen:Bool= false;
   public static var mCollectEveryFrame:Bool = false;

#if flash
   static var mFPSObj: flash.text.TextField = null;
   static var mT0 = 0.0;
   static var mFrameCount = 0;
   static var mFrameCountStack:Array<Float> = [];
#else

   public static var mQuitOnEscape:Bool = true;
   static var mStage:neash.display.Stage;
   static var mMainClassRoot:neash.display.MovieClip;
   static var mCurrent:neash.display.MovieClip;
   static var mRolling:InteractiveObject;
   static var mDownObj:InteractiveObject;
#if (neko||cpp)
   public static var mLastMouse:nme.geom.Point = new nme.geom.Point();
#elseif js
   public static var mLastMouse:canvas.geom.Point = new canvas.geom.Point();
#end


   var mManager : Manager;
   var mArgs:Array<String>;
#end

   function new(inName:String,inWidth:Int,inHeight:Int,?inFullScreen:Null<Bool>,?inResizable:Null<Bool>,?cb:Void->Void)
   {
      mKilled = false;
      mRequestedWidth = inWidth;
      mRequestedHeight = inHeight;
		mResizePending = false;

#if flash
      flash.Lib.current.stage.frameRate = 100;
#elseif (neko || cpp)

      #if neko
      mArgs = neko.Sys.args();
      #elseif cpp
      mArgs = untyped __global__.__get_args();
      #else
      mArgs = [];
      #end

      var vsync = false;

      while(mArgs.length>0)
      {
         if (mArgs[0]=="-opengl")
         {
            mOpenGL = true;
            mArgs.shift();
         }
         else if (mArgs[0]=="-fullscreen")
         {
            mFullscreen = true;
            mArgs.shift();
         }
         else if (mArgs[0]=="-vsync")
         {
            vsync = true;
            mArgs.shift();
         }
         else if (mArgs[0]=="-debug")
         {
            debug = true;
            mArgs.shift();
         }

         else
            break;
      }

      var full:Bool = inFullScreen==null ? mFullscreen : inFullScreen;
      var resizable:Bool = inResizable==null ? true : inResizable;


      mManager = new Manager( inWidth, inHeight, inName, full, "", mOpenGL, resizable, true, vsync);
      var w = nme.Manager.getWindowWidth();
      var h = nme.Manager.getWindowHeight();
      mResizePending = (w!=mRequestedWidth) || (h!=mRequestedHeight);
      mStage = new neash.display.Stage(w,h,mManager);
      mStage.frameRate = 100;
      mMainClassRoot = new MovieClip();
      mStage.addChild(mMainClassRoot);
      mCurrent = mMainClassRoot;
      mCurrent.name = "Root MovieClip";
      mRolling = null;
#elseif js
      mManager = new Manager( inWidth, inHeight, inName, cb );
      mStage = new neash.display.Stage(inWidth,inHeight,mManager);
      mStage.frameRate = 100;
      mMainClassRoot = new MovieClip();
      mStage.addChild(mMainClassRoot);
      mCurrent = mMainClassRoot;
      mCurrent.name = "Root MovieClip";

#end
   }

#if !flash
   public function OnResize(inW:Int, inH:Int)
   {
      mManager.OnResize(inW,inH);
      mStage.OnResize(inW,inH);
   }
#end

   static public function IsOpenGL() { return mOpenGL; }

   static public function ShowCursor(inShow:Bool)
   {
      mShowCursor = inShow;
#if flash
      if (inShow)
         flash.ui.Mouse.show();
      else
         flash.ui.Mouse.hide();
#elseif (neko || cpp)
      nme.Manager.SetCursor( inShow ? 1 : 0 );
#elseif js
      canvas.Manager.SetCursor( inShow ? 1 : 0 );
#end
   }
#if (neko || cpp)
   static public function SetTextCursor(inText:Bool)
   {
      if (inText)
         nme.Manager.SetCursor( 2 );
      else
         nme.Manager.SetCursor( mShowCursor ? 1 : 0 );
   }
#elseif js
   static public function SetTextCursor(inText:Bool)
   {
      if (inText)
         canvas.Manager.SetCursor( 2 );
      else
         canvas.Manager.SetCursor( mShowCursor ? 1 : 0 );
   }
#end
   static public function SetFrameRate(inRate:Float)
   {
      // trace("Frame rate:" + inRate);
#if flash
      flash.Lib.current.stage.frameRate = inRate;
#else
      mStage.frameRate = inRate;
#end

   }


   static public function GetParameters() : Dynamic
   {
#if flash
      // trace( flash.Lib.current.loaderInfo.parameters );
      return flash.Lib.current.loaderInfo.parameters;
#elseif (neko || cpp)
      var result:Dynamic = {};
      var param_id = 0;

      #if neko
      var args = neko.Sys.args();
      #elseif cpp
      var args = cpp.Sys.args();
      #end

      for(arg in args)
      {
         if (arg.charAt(0)=='-')
         {
            var pos = arg.indexOf("=");
            if (pos>0)
               Reflect.setField(result,arg.substr(1,pos-1), arg.substr(pos+1) );
            else
               Reflect.setField(result,arg.substr(1),true);
         }
         else
         {
            var name = "param" + param_id;
            Reflect.setField(result,name,arg);
         }
      }

      return result;
#elseif js
      // TODO
      return null;
#end
   }

   static public function GetCurrent() : MovieClip
   {
#if flash
      return flash.Lib.current;
#else
      return mMainClassRoot;
#end
   }

   static public function ShowFPS(?inShow:Bool)
   {
      mShowFPS = inShow==null || inShow;
#if flash
      if (mShowFPS && mFPSObj==null)
      {
         mFPSObj = new flash.text.TextField();
         mFPSObj.backgroundColor = 0xffffff;
         mFPSObj.background = true;
         mFPSObj.autoSize = flash.text.TextFieldAutoSize.LEFT;
         mFPSObj.x = 10;
         mFPSObj.y = 10;
         mFPSObj.text = "FPS";
         mT0 = getTimer();
         flash.Lib.current.stage.addChild(mFPSObj);
         mFPSObj.addEventListener( flash.events.Event.ENTER_FRAME, function(e)
             {
              var t = getTimer() - mT0;
              var n = mFrameCountStack.length;
              mFrameCountStack[n] = t;
              if (n>0)
              {
                 var t0 = mFrameCountStack[0];
                 t -= t0;
                 if (t>0)
                    mFPSObj.text = "FPS:" + Std.int( n*1000/t );
                 if (n>10)
                    mFrameCountStack.shift();
              }
             }
            );
      }
      mFPSObj.visible = mShowFPS;
#end
   }


   static public function SetBackgroundColour(inColour:Int)
   {
#if !flash
     mStage.backgroundColor =  inColour;
#else
     // what to do here ?
     //flash.Lib.current.backgroundColor =  inColour;
#end
   }

   public static function as<T>( v : Dynamic, c : Class<T> ) : Null<T>
   {
   #if flash
   	return untyped __as__(v,c);
   #elseif neko
      return Std.is(v,c) ? v : null;
   #else
      return v;
   #end
   }

#if neko
   // Be careful to to blow precision ...
   static var starttime : Float = haxe.Timer.stamp();
   static public function getTimer() : Int { return Std.int((neko.Sys.time()-starttime) * 1000.0); }
#elseif cpp
   static public function getTimer() : Int { return  untyped __global__.__time_stamp()*1000.0; }
#elseif js
   static var starttime : Float = haxe.Timer.stamp();
   static public function getTimer() :Int { return ( Std.int(haxe.Timer.stamp() - starttime )*1000); }
#else
   static public function getTimer() { return flash.Lib.getTimer(); }
#end

#if !flash
   static public function GetStage() { return mStage; }

   public function ProcessKeys( code:Int , pressed : Bool, inChar:Int,
                      ctrl:Bool, alt:Bool, shift:Bool )
   {
      if (code== KeyCode.ESCAPE && mQuitOnEscape)
      {
         mKilled = true;
         return;
      }

      switch code
      {
         // You might want to disable this in production

         case KeyCode.TAB:
            mStage.TabChange( shift ? -1 : 1,code);

         default:
            var event = new KeyboardEvent(
                              pressed ? KeyboardEvent.KEY_DOWN:
                                        KeyboardEvent.KEY_UP,
                               true,false,
                               inChar,
                               KeyCode.ConvertCode(code),
                               KeyCode.ConvertLocation(code),
                               ctrl,alt,shift);

            mStage.HandleKey(event);
      }
   }

   function CreateMouseEvent(inObj:InteractiveObject,inRelatedObj:InteractiveObject,
#if !js
                 inMouse:nme.MouseEvent,inType:String): neash.events.MouseEvent
#else
                 inMouse:MouseEvent,inType:String): neash.events.MouseEvent
#end
   {
      var bubble = inType!=MouseEvent.ROLL_OUT && inType!=MouseEvent.ROLL_OVER;
#if !js
      var pos = new neash.geom.Point(inMouse.x,inMouse.y);
#else
      var pos = new neash.geom.Point(inMouse.localX,inMouse.localY);
#end
      if (inObj!=null)
         pos = inObj.globalToLocal(pos);

#if !js
      var result =  new MouseEvent(inType,
                         bubble, false,
                         pos.x,pos.y,
                         inRelatedObj,
                         inMouse.ctrl,
                         inMouse.alt,
                         inMouse.shift,
                         inMouse.leftIsDown,
                         2);
#else
      var result =  new MouseEvent(inType,
                         bubble, false,
                         inMouse.localX,inMouse.localY,
                         inRelatedObj,
                         inMouse.ctrlKey,
                         inMouse.altKey,
                         inMouse.shiftKey,
                         inMouse.buttonDown,
                         2);
#end
       result.stageX = inMouse.x/mStage.scaleX;
       result.stageY = inMouse.y/mStage.scaleY;
       result.target = inObj;
       return result;
   }

   function GetInteractiveObjectAtPos(inX:Int,inY:Int) : InteractiveObject
   {
      return mStage.GetInteractiveObjectAtPos(inX,inY);
   }

   static function FireEvents(inEvt:Event,inList:Array<InteractiveObject>)
   {
      var l = inList.length;
      if (l==0)
         return;

      // First, the "capture" phase ...
      inEvt.SetPhase(EventPhase.CAPTURING_PHASE);
      for(i in 0...l-1)
      {
         var obj = inList[i];
         inEvt.currentTarget = obj;
         obj.dispatchEvent(inEvt);
         if (inEvt.IsCancelled())
            return;
      }

      // Next, the "target"
      inEvt.SetPhase(EventPhase.AT_TARGET);
      inEvt.currentTarget = inList[l-1];
      inList[l-1].dispatchEvent(inEvt);
      if (inEvt.IsCancelled())
          return;

      // Last, the "bubbles" phase
      if (inEvt.bubbles)
      {
         inEvt.SetPhase(EventPhase.BUBBLING_PHASE);
         var i=l-2;
         while(i>=0)
         {
            var obj = inList[i];
            inEvt.currentTarget = obj;
            obj.dispatchEvent(inEvt);
            if (inEvt.IsCancelled())
               return;
            --i;
         }

      }
   }


   static public function SendEventToObject(inEvent:Event,inObj:InteractiveObject) : Void
   {
      var objs = GetAnscestors(inObj);
      objs.reverse();
      FireEvents(inEvent,objs);
   }

   static function GetAnscestors(inObj:DisplayObject) : Array<InteractiveObject>
   {
      var result:Array<InteractiveObject> = [];

      while(inObj!=null)
      {
         var interactive = inObj.AsInteractiveObject();
         if (interactive!=null)
            result.push(interactive);
         inObj = inObj.GetParent();
      }

      result.reverse();
      return result;
   }

   static public function SetDragged(inObj:DisplayObject,?inCentre:Bool, ?inRect:Rectangle)
   {
      mDragObject = inObj;
      mDragRect = inRect;
      if (mDragObject!=null)
      {
         if (inCentre!=null && inCentre)
         {
            mDragOffsetX = -inObj.width/2;
            mDragOffsetY = -inObj.height/2;
         }
         else
         {
            var mouse = Manager.GetMouse();
            mouse.x /= mStage.scaleX;
            mouse.y /= mStage.scaleY;
            var p = mDragObject.parent;
            if (p!=null)
               mouse = p.globalToLocal(mouse);

            mDragOffsetX = inObj.x-mouse.x;
            mDragOffsetY = inObj.y-mouse.y;
         }
      }
   }

   function DragObject(inX:Float, inY:Float)
   {
#if !js
      var pos = new nme.geom.Point(inX,inY);
#else
      var pos = new Point(inX,inY);
#end
      var p = mDragObject.parent;
      if (p!=null)
         pos = p.globalToLocal(pos);

      if (mDragRect!=null)
      {
         if (pos.x < mDragRect.x) pos.x = mDragRect.x;
         else if (pos.x > mDragRect.right) pos.x = mDragRect.right;

         if (pos.y < mDragRect.y) pos.y = mDragRect.y;
         else if (pos.y > mDragRect.bottom) pos.y = mDragRect.bottom;
      }

      mDragObject.x = pos.x + mDragOffsetX;
      mDragObject.y = pos.y + mDragOffsetY;
   }


   function DoMouse(inEvent:EventType)
   {
      var mouse = Manager.mouseEvent(met_Move);

      mLastMouse.x  = mouse.x;
      mLastMouse.y =  mouse.y;

      if (mDragObject!=null)
        DragObject(mouse.x/mStage.scaleX,mouse.y/mStage.scaleY);

      var obj = GetInteractiveObjectAtPos(mouse.x,mouse.y);

      var type:String =
            (inEvent==et_mousemove) ?       MouseEvent.MOUSE_MOVE :
            (inEvent==et_mousebutton_up) ?  MouseEvent.MOUSE_UP :
            (inEvent==et_mousebutton_down) ?MouseEvent.MOUSE_DOWN :
            "unknown";

      var new_list:Array<InteractiveObject> = obj!=null ?  GetAnscestors(obj) : [];
      var nl = new_list.length;


      // Handle roll-over/roll-out events ...
      if (obj!=mRolling)
      {
         if (mRolling!=null)
         {
            mRolling.DoMouseLeave();
            #if !js
            var evt = CreateMouseEvent(mRolling,obj,mouse,MouseEvent.MOUSE_OUT);
            #else
            var evt = CreateMouseEvent(mRolling,obj, untyped mouse,MouseEvent.MOUSE_OUT);
            #end
            mRolling.dispatchEvent(evt);
         }

         var old_list = GetAnscestors(mRolling);
         var ol = old_list.length;

         // Find common parents...
         var common=0;
         var stop = ol<nl ? ol:nl;

         while(common<stop && old_list[common]==new_list[common])
            common++;

         if (ol>common)
         {
#if !js
            var evt = CreateMouseEvent(mRolling,obj,mouse,MouseEvent.ROLL_OUT);
#else
            var evt = CreateMouseEvent(mRolling,obj, untyped mouse,MouseEvent.ROLL_OUT);
#end
            for(o in common...ol)
            {
               evt.target = old_list[o];
               old_list[o].dispatchEvent(evt);
            }
         }

         if (nl>common)
         {
#if !js
            var evt = CreateMouseEvent(obj,mRolling,mouse,MouseEvent.ROLL_OVER);
#else
            var evt = CreateMouseEvent(obj,mRolling,untyped mouse,MouseEvent.ROLL_OVER);
#end
            for(o in common...nl)
            {
               evt.target = new_list[o];
               new_list[o].dispatchEvent(evt);
            }
         }

         mRolling = obj;
         if (mRolling!=null)
         {
            mRolling.DoMouseEnter();
            #if !js
            var evt = CreateMouseEvent(mRolling,obj,mouse,MouseEvent.MOUSE_OVER);
            #else
            var evt = CreateMouseEvent(mRolling,obj, untyped mouse,MouseEvent.MOUSE_OVER);
            #end
            mRolling.dispatchEvent(evt);
         }

      }

      // Send event directly to InteractiveObject for internal processing
      if (inEvent==et_mousebutton_down)
      {
         mDownObj = obj;
         if (obj!=null)
            obj.OnMouseDown(mouse.x,mouse.y);
      }
      else if (inEvent==et_mousemove && mDownObj!=null)
         mDownObj.OnMouseDrag(mouse.x,mouse.y);
      else if (inEvent==et_mousebutton_up)
      {
         // trace("Up :" + obj + "/" + mDownObj);

         if (mDownObj!=null)
         {
            mDownObj.OnMouseUp(mouse.x,mouse.y);

            if (obj==mDownObj)
            {
               //trace("CLICK!");
#if !js
               var evt = CreateMouseEvent(obj,null,mouse,MouseEvent.CLICK);
#else
               var evt = CreateMouseEvent(obj,null,untyped mouse,MouseEvent.CLICK);
#end
               FireEvents(evt,new_list);
            }
            else
            {
               // Send up event to same place as down event...
               obj = mDownObj;
               new_list = GetAnscestors(obj);
            }
         }

         mDownObj = null;
      }


      if (nl>0 && (inEvent==et_mousebutton_down || inEvent==et_mousebutton_up) ||
             inEvent==et_mousemove )
      {
#if !js
         var evt = CreateMouseEvent(obj,null,mouse,type);
#else
         var evt = CreateMouseEvent(obj,null,untyped mouse,type);
#end
         FireEvents(evt,new_list);
      }

      // trace(type + " obj = " + (obj==null?"null":"something") );


      //var event =CreateMouseEvent(inEvent,type);
   }

#end

#if js
    var timer : Dynamic;
    var frame : Int;
    static var interval : Int;
    function setTimer ( ?next :Int ) {
        //if (mShowFPS)
                mManager.RenderFPS();

        if ( timer != null ) untyped window.clearInterval( timer );
        if ( next == null ) {
            if ( mStage != null ) {
                if ( interval == null ) interval = Std.int( 1000.0/mStage.frameRate );
                next = interval;
            } else {
                next = 1;
            }
        }

        timer = untyped window.setInterval( step, next );
    }
#end
    function step () {

#if silverlight
        frame++;

        if ( Manager.getScreen() != null ) {
            Manager.resolveQueue();
            if ( frame % 2 == 0 ) {

                var e : Event = new Event( Event.ENTER_FRAME );
                mStage.dispatchEvent( e );

                // TODO: this still causes memory leaks
                //mStage.Broadcast(e);

            } else {
                mStage.RenderAll();
            }
        }
#elseif !flash
       
#if js
        mManager.clear(mStage.backgroundColor);
#end

        // Process pending timers ...
        neash.Timer.CheckTimers();
        // Send frame-enter event
        var event = new Event( Event.ENTER_FRAME );
        mStage.Broadcast(event);
         
        var type:EventType;
        do
        {
            // Fill our event structure with event information.
            var evt = mManager.getNextEvent();
            type = mManager.getEventType();
            switch(type)
            {
            case et_keydown:
                var code = mManager.lastKey();
                    ProcessKeys( code, true,
                        mManager.lastChar(),
                        mManager.lastKeyCtrl(), mManager.lastKeyAlt(),
                        mManager.lastKeyShift() );
            case et_keyup:
                var code = mManager.lastKey();
                    ProcessKeys( code, false,
                        mManager.lastChar(),
                        mManager.lastKeyCtrl(), mManager.lastKeyAlt(),
                        mManager.lastKeyShift() );

            case et_mousemove:
                DoMouse(et_mousemove);
                /* No such thing as fallthrough in haxe !*/
            case et_mousebutton_down:
                DoMouse(et_mousebutton_down);

            case et_mousebutton_up:
                DoMouse(et_mousebutton_up);

            case et_resize:
                OnResize(evt.width,evt.height);

            case et_quit:
                mKilled = true;
            default:
            }
        } while(type!=et_noevent && !mKilled);

        mStage.RenderAll();
#end

#if js
        setTimer();
#end
   }


    function MyRun( )
    {
#if flash
        // No need to do anything!
#elseif js
        mManager.ResetFPS();
        setTimer();
#else
        var last_render = 0.0;
        if (mResizePending)
        {
            mResizePending =false;
            //trace("OnResize!");
            mStage.OnResize(nme.Manager.getWindowWidth(), nme.Manager.getWindowHeight());
        }

        while(!mKilled)
        {
            step();

            if (mStage.frameRate>0)
            {
                var next = last_render+(1000.0/mStage.frameRate);
                var now = getTimer();
                if (now + 10.0 <next)
                {
                    #if neko
                    neko.Sys.sleep( (next-now) * 0.001 );
                    #elseif cpp
                    cpp.Sys.sleep( (next-now) * 0.001 );
                    #end
                    now = getTimer();
                    last_render = now;
                }
                else
                    last_render = now;
            }


            if (mShowFPS)
                mManager.RenderFPS();

            if (mCollectEveryFrame)
            {
#if cpp
                untyped __global__.__hxcpp_collect();
#elseif neko
                // This seems to help!
                neko.vm.Gc.run(true);
#end
            }
            mManager.flip();
        }
        mManager.close();

#end
    }


   static public function Run( ) {
     mMe.MyRun();
   }


   public static function close()
   {
   #if flash
      flash.Lib.fscommand("quit","");
   #else
      mMe.mKilled = true;
   #end
   }


   public static function Init(inName:String,inWidth:Int,inHeight:Int,
                               ?inFullScreen:Null<Bool>,?inResizable:Null<Bool>,?cb:Void->Void)
   {
      mMe = new Lib(inName,inWidth,inHeight,inFullScreen,inResizable,cb);
   }

#if js
   // javascript target does not need the boilerplate Neash initialisation code
   static function __init__()
   {
     untyped
     {
       var els = document.getElementsByTagName('canvas');
       if ( els.length > 0 )
       {
	 var tgt = els[0];
	 var width : Int = tgt.getAttribute('width') != null ? cast tgt.getAttribute('width') : Manager.DEFAULT_WIDTH;
	 var height : Int = tgt.getAttribute('height') != null ? cast tgt.getAttribute('height') : Manager.DEFAULT_HEIGHT;
	 var name = tgt.getAttribute('id') != null ? tgt.getAttribute('id') : 'Container';
	 mMe = new Lib( name, width, height );
	 mMe.MyRun();
       }
     }
   }
#end

}
